/**
* GET /ds-devices/{serial_number}/logs — 制御盤のログファイルの一覧(指示書AY 段階7-1。D-179)
*
* 仕様の正は `d-watch-api.yaml` の /ds-devices/{serial_number}/logs。
* 一覧は毎回制御盤に問い合わせ(中継。最大15秒)、絞り込み・並べ替え・ページ分けは `queryDeviceLogs()` で行う(D8)。
* ★サーバーには保存しない★。★検証の順序は シリアル(422)→ クエリ(422)→ 実在(404)→ 制御盤★。
*/
import { NextResponse } from "next/server";
import { parsePathSerialNumber } from "@/lib/api/path-params";
import { db } from "@/lib/db";
import { USE_FIXTURES } from "@/lib/db/use-fixtures";
import { errorBody, jsonError } from "@/lib/ds-agent/agent-request";
import { ensureFixtureAgent } from "@/lib/ds-agent/fixture-agent";
import { getRelay } from "@/lib/ds-agent/relay";
import { ensureDsAgentLogging, truncateAgentCode } from "@/lib/ds-agent/relay-log";
import { queryDeviceLogs } from "@/lib/ds-logs/query";
import { parseDeviceLogQuery } from "@/lib/ds-logs/validate";
import { logEvent, withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { DsLogListResponse } from "@/lib/types";
const ROUTE = "/api/ds-devices/[serial_number]/logs";
export async function GET(
request: Request,
{ params }: { params: Promise<{ serial_number: string }> }
) {
return withRequestId(request, async () => {
ensureDsAgentLogging();
const { serial_number: serialRaw } = await params;
const parsedSerial = parsePathSerialNumber(serialRaw);
if (!parsedSerial.ok) return NextResponse.json(parsedSerial.error, { status: parsedSerial.status });
const serial = parsedSerial.query;
const parsedQuery = parseDeviceLogQuery(new URL(request.url).searchParams);
if (!parsedQuery.ok) return NextResponse.json(parsedQuery.error, { status: parsedQuery.status });
const query = parsedQuery.query;
let exists: boolean;
try {
exists = await db.dsDeviceExists(serial);
} catch (e) {
const body = errorBody("internal_error", "データの取得に失敗しました");
logApiError({ route: ROUTE, method: "GET", body, status: 500, err: e });
return NextResponse.json(body, { status: 500 });
}
if (!exists) return jsonError(404, "not_found", "デバイスが見つかりません");
const relay = getRelay();
if (USE_FIXTURES) ensureFixtureAgent(relay, serial);
const started = Date.now();
const result = await relay.requestList(serial);
const duration_ms = Date.now() - started;
const logList = (
r: "ok" | "offline" | "timeout" | "busy" | "agent_error",
extra: { item_count?: number; truncated?: boolean; agent_code?: string } = {}
) =>
logEvent("info", {
event: "ds_logs.list",
serial_number: serial,
item_count: extra.item_count ?? 0,
truncated: extra.truncated ?? false,
duration_ms,
result: r,
...(extra.agent_code !== undefined ? { agent_code: truncateAgentCode(extra.agent_code) } : {}),
});
switch (result.kind) {
case "offline": {
logList("offline");
const lastSeen = relay.agentStatus(serial).last_seen_at;
return jsonError(409, "agent_offline", "制御盤が接続されていません", {
last_seen_at: lastSeen ? [lastSeen] : [],
});
}
case "timeout":
logList("timeout");
return jsonError(504, "agent_timeout", "制御盤から応答がありません");
case "busy":
logList("busy");
return jsonError(429, "agent_busy", "制御盤が混み合っています");
case "agent_error":
logList("agent_error", { agent_code: result.code });
return jsonError(502, "agent_error", "制御盤でエラーが発生しました", { agent_code: [result.code] });
case "ok": {
const page = queryDeviceLogs(result.result.types, result.result.items, query);
logList("ok", { item_count: result.result.items.length, truncated: result.result.truncated });
const body: DsLogListResponse = {
agent: relay.agentStatus(serial),
types: result.result.types,
items: page.items,
truncated: result.result.truncated,
meta: { total: page.total, page: query.page, per_page: query.perPage },
};
return NextResponse.json(body);
}
}
});
}